local utility = require('nginx-admin-utils')
local ox = utility.ox

local popen = utility.popen
local set_non_blocking = utility.set_non_blocking
local sleep = utility.sleep
local is_file_exist = utility.is_file_exist
local print_and_log = utility.print_and_log

local BUFSIZE = utility.BUFSIZE

local ngx_bin_name = utility.ngx_bin_name
local ngx_pid_name = utility.ngx_pid_name
local ngx_conf_name = utility.ngx_conf_name
local ngx_conf_prefix = utility.ngx_conf_prefix
local ngx_conf_app_path = utility.ngx_conf_app_path

local ngx_user_name = utility.ngx_user_name
local ngx_group_name = utility.ngx_group_name

local gray_sleep_loc = utility.gray_sleep_loc

local ecode_success = utility.ecode_success
local ecode_warn = utility.ecode_warn
local ecode_fatal_error = utility.ecode_fatal_error
local ecode_env_exception = utility.ecode_env_exception
local ecode_retry_after_modify = utility.ecode_retry_after_modify
local ecode_retry_later = utility.ecode_retry_later


local function get_path_from_uri(uri)
    local path = string.match(uri, "^/action/[^/]+(/.+)")
    if not path then
        return nil, "no path in uri"
    end
    if string.match(path, "^/%@/%S+") then
        path = string.match(path, "^/%@/(.*)")
        path = ngx_conf_prefix..path
    end
    if #path < #ngx_conf_prefix or
        string.sub(path, 1, #ngx_conf_prefix) ~= ngx_conf_prefix then
        return nil, "invalid file path"
    end
    if string.match(path, "%.%.") then
        return nil, "invalid file path"
    end
    return path
end


local function check_nginx_status(pid)
    local ngx_pid_oldbin_fn = ngx_pid_name..".oldbin"
    if is_file_exist(ngx_pid_oldbin_fn) then
        return "upgrading"
    end
    if not pid then
        local f, m = io.open(ngx_pid_name, "r")
        if f == nil then
            -- TODO get from ps aux?
            if string.match(m, "No such file") then
                return "stopped"
            else
                return nil, m 
            end
        end
        pid = f:read("*all")
        f:close()
        if #pid == 0 then return "stopped" end
        pid = tonumber(pid)
    end
    -- might be a zombie process
    ox.wait(pid, ox.WNOHANG)
    -- to check if pid exist(TODO check ESRCH)
    local r, em, en  = ox.kill(pid, 0)
    if not r then
        if en == ox.ESRCH then return "stopped" end
        return nil, em
    else
        return "running", pid
    end
end


local function operate_nginx(action, data)
    local function test_nginx()
        local accept_warn
        if ngx.var.arg_accept_warn == "true" then accept_warn = true end
        local cmd = ngx_bin_name.." -t -c "..ngx_conf_name
        local stderr_r, pid = popen(cmd, nil, true)
        if stderr_r == nil then return ecode_env_exception, pid end
        set_non_blocking(stderr_r)
        local stderr_msg = ""
        local need_sleep = true
        local try_times = 100
        while true do
            if need_sleep then
                try_times = try_times - 1
                if try_times <= 0 then
                    ox.close(stderr_r)
                    return ecode_env_exception, "test nginx maybe blocked!"
                end
                sleep(0.1)
            end
            local r, em, en = ox.read(stderr_r, BUFSIZE)
            if not r then
                if en == ox.EINTR then
                    need_sleep = false
                elseif en == ox.EAGAIN then
                    need_sleep = true
                else
                    ox.close(stderr_r)
                    return ecode_env_exception, em
                end
            else
                if #r == 0 then break end
                stderr_msg = stderr_msg..r
                need_sleep = true
            end
        end
        ox.close(stderr_r)
        if string.match(stderr_msg, "%[error%]")
            or string.match(stderr_msg, "%[crit%]")
            or string.match(stderr_msg, "%[alert%]")
            or string.match(stderr_msg, "%[emerg%]") then
            return ecode_retry_after_modify, stderr_msg
        end
        local conf_ok = 0
        if string.match(stderr_msg, "syntax is ok") then
            conf_ok = conf_ok + 1
        end
        if string.match(stderr_msg, "test is successful") then
            conf_ok = conf_ok + 1
        end
        if conf_ok < 2 then
            return ecode_retry_after_modify, stderr_msg
        else
            local rc = ecode_warn
            if select(2, string.gsub(stderr_msg, "\n", "\n")) == 2 then
                stderr_msg = nil
                rc = ecode_success
            elseif not accept_warn then
                rc = ecode_retry_after_modify
            end
            return rc, stderr_msg
        end
    end

    local function stop_nginx(pid, force)
        local signal = ox.SIGQUIT
        if force then signal = ox.SIGTERM end
        local r, em, en = ox.kill(pid, signal)
        if not r then
            if en == ox.SRCH then return ecode_success end
            return ecode_env_exception, em
        end
        local try_times = 150
        while true do
            sleep(0.1)
            local r, m = check_nginx_status()
            if not r then return ecode_env_exception, m end
            if r == "stopped" then
                return ecode_success
            end
            try_times = try_times - 1
            if try_times <= 0 then return ecode_retry_later, "stop timeout" end
        end
    end

    local function start_nginx()
        local r, m_t = test_nginx()
        if r ~= ecode_success then return r, m_t end
        local cmd = ngx_bin_name.." -c "..ngx_conf_name
        local stderr_r, pid = popen(cmd, nil, true)
        if stderr_r == nil then return ecode_env_exception, pid end
        -- ox.close(stderr_r)
        set_non_blocking(stderr_r)
        local stderr_msg = ""
        local failed, emsg
        -- TODO get errmsg from stderr, wait_pid(pid, O_NONBLOCK)
        local try_times = 150
        while true do
            sleep(0.1)
            -- read error msg
            local rr, em, en = ox.read(stderr_r, BUFSIZE)
            local r, m = check_nginx_status()
            if not r then failed = ecode_env_exception; emsg = m; break end
            if r == "running" then
                failed = ecode_success; emsg = m_t; break
            end
            if not rr then
                if en ~= ox.EINTR and en ~= ox.EAGAIN then
                    failed = ecode_env_exception; emsg = em; break
                end
            else
                if #rr == 0 then
                    if string.match(stderr_msg, "%[error%]")
                       or string.match(stderr_msg, "%[crit%]")
                       or string.match(stderr_msg, "%[alert%]")
                       or string.match(stderr_msg, "%[emerg%]") then
                        failed = ecode_retry_after_modify
                        emsg = stderr_msg
                        break
                    end
                else
                    stderr_msg = stderr_msg..rr
                end
            end
            try_times = try_times - 1
            if try_times <= 0 then
                failed = ecode_env_exception
                emsg = "start nginx timeout! nginx may blocked by something!"
                break
            end
        end
        ox.close(stderr_r)
        if failed ~= ecode_success then
            stop_nginx(pid, true)
        end
        return failed, emsg
    end

    local function reload_nginx(pid)
        local r, m_t = test_nginx()
        if r ~= ecode_success then return r, m_t end
        local signal = ox.SIGHUP
        local r, em = ox.kill(pid, signal)
        if not r then
            return ecode_env_exception, em
        end
        sleep(0.1)
        return ecode_success, m_t
    end

    local function restart_nginx(pid, force)
        local r, m_t = test_nginx()
        if r ~= ecode_success then return r, m_t end
        -- shutdown now
        stop_nginx(pid, 1)
        return start_nginx()
    end

    if action == "start" then
        return start_nginx()
    elseif action == "stop" then
        return stop_nginx(data)
    elseif action == "stop_f" then
        return stop_nginx(data, "force")
    elseif action == "restart" then
        return restart_nginx(data)
    elseif action == "restart_f" then
        return restart_nginx(data, "force")
    elseif action == "reload" then
        return reload_nginx(data)
    elseif action == "test" then
        return test_nginx()
    else
        return ecode_success
    end
end


local function operate_conf_file(action, path, data)
    local function info_file(path) 
        local f, m = io.open(path, "r")
        if not f then 
            ngx.status = ngx.HTTP_NOT_FOUND
            return print_and_log(ecode_retry_after_modify, m)
        end
        while true do
            local block = f:read(BUFSIZE)
            if not block then break end
            ngx.print(block)
        end
        ngx.log(ngx.WARN, "content of "..path)
        f:close()
        return ecode_success
    end
    local function receive_file(path, body, force)
        -- TODO check body
        if not force and utility.is_file_exist(path) then
            return ecode_retry_after_modify, "file exist"
        end
        local f, m = io.open(path, "w")
        if not f then return ecode_env_exception, m end
        f:write(body)
        f:close()
        if ngx_user_name and ngx_group_name then
            ox.chown(path, ngx_user_name, ngx_group_name)
        end
        return ecode_success
    end
    local function delete_file(path)
        if is_file_exist(path) then
            local r, m = os.remove(path)
            if not r then return ecode_env_exception, m end
        else
            return ecode_retry_after_modify, path.." not exist!"
        end
        return ecode_success
    end
    if action == "info" then
        return info_file(path)
    elseif action == "delete" then
        return delete_file(path)
    elseif action == "save" then
        return receive_file(path, data)
    elseif action == "save_f" then
        return receive_file(path, data, "force")
    else 
        return ecode_success
    end
end


local function calculate_md5(path)
    local f, m = io.open(path, "r")
    if not f then return nil, m end
    local md5 = ngx.md5(f:read("*all"))
    f:close()
    return md5
end


local action = string.match(ngx.var.uri, "^/action/([^/]+)")

local function restful_admin()
    ---- actions
    -- 1, operate nginx

    local r, m, c

    if action == "reload" or action == "test" or action == "restart" 
        or action == "start" or action == "stop" or action == "status" then
          
        local cur_status, data = check_nginx_status()
        if cur_status == "stopped" then
            if action == "reload" then
                return print_and_log(ecode_retry_after_modify,
                                     "nginx is not running")
            elseif action == "stop" then
                return print_and_log(ecode_success)
            elseif action == "restart" then
                action = "start"
            end
        elseif cur_status == "running" then
            if action == "start" then
                return print_and_log(ecode_success)
            end
        elseif cur_status == "upgrading" then
            return print_and_log(ecode_retry_later, "nginx is upgrading")
        else
            return print_and_log(ecode_env_exception, data)
        end
        if action == "status" then
            return print_and_log("RAW", string.format("{\"status\":\"%s\"}",
                                 cur_status))
        end
        if action == "stop" or action == "restart" then
            if string.match(ngx.var.uri, "^/action/"..action.."/force") then
                action = action.."_f"
            end
        end
        return print_and_log(operate_nginx(action, data))
    elseif action == "info" then
        local path, m = get_path_from_uri(ngx.var.uri)
        if not path then
            ngx.status = ngx.HTTP_BAD_REQUEST
            return print_and_log(ecode_retry_after_modify, m)
        end
        if utility.is_dir(path) then
            ngx.status = ngx.HTTP_BAD_REQUEST
            return print_and_log(ecode_retry_after_modify, path.." is a directory")
        end
        return operate_conf_file(action, path)
    elseif action == "save" or action == "delete" then
        local count
        local uri = ngx.var.uri
        local data = ngx.var.request_body
        if action == "save" then
            if not data then
                return print_and_log(ecode_retry_after_modify, "no data in body")
            end
            uri, count = string.gsub(uri, "^/action/save/force/", "/action/save/")
            if count > 0 then action = action.."_f" end
        end
        local path, m = get_path_from_uri(uri)
        if not path then
            return print_and_log(ecode_retry_after_modify, m)
        end
        if utility.is_dir(path) then
           return print_and_log(ecode_retry_after_modify, path.." is a directory")
        end
        return print_and_log(operate_conf_file(action, path, data))
    elseif action == "confirm" then
        local ret_str, confirm_all
        if ngx.var.uri == "/action/confirm/all" then
            confirm_all = true
        end

        if not confirm_all then
            ret_str = "{\"status\":\"SUCCESS\",\"result\":{"
            -- main conf
            ret_str = ret_str..string.format("\"%s\":\"%s\"", ngx_conf_name,
                                             calculate_md5(ngx_conf_name))
            -- app servers
            local entries, m = utility.readdir(ngx_conf_app_path)
            if entries then
                for _, fn in ipairs(entries) do
                    if fn ~= '..' and fn ~= '.' and
                       utility.is_file(ngx_conf_app_path..fn) then
                        local r, m = calculate_md5(ngx_conf_app_path..fn)
                        if not r then
                            return print_and_log(ecode_env_exception, m)
                        end
                        ret_str = ret_str..string.format(",\"%s\":\"%s\"",
                                           ngx_conf_app_path..fn, r)
                    end
                end
            end
            ret_str = ret_str.."}}"
            return print_and_log("RAW", ret_str)
        end
    else
        return print_and_log(ecode_retry_after_modify, "unknown operation")
    end
end


local function lock()
    -- lua_shared_dict lua_lock 128k;
    local lua_lock = ngx.shared.lua_lock
    -- expired time is 30s
    local succ, err, forcible = lua_lock:add("lua_lock", 1, 20)
    if succ == true then
        return true
    else
        return false
    end
end
    

local function unlock()
    local lua_lock = ngx.shared.lua_lock
    lua_lock:delete("lua_lock")
end


local locked

if action ~= 'info' and action ~= 'status' and action ~= 'confirm' and action ~= 'save'and action ~= 'delete' then
    locked = lock()
end

if locked == false then
    if action == 'reload' then
        return print_and_log(ecode_success)
    end
    return print_and_log(ecode_retry_later, "someone is operating, please try again later")
else
    local r, v = pcall(restful_admin)
    if action ~= 'info' and action ~= 'status' and action ~= 'confirm' and action ~= 'save'and action ~= 'delete' then
        unlock()
    end
    if not r then 
        return print_and_log(ecode_env_exception, "fatal error!! there may be some bugs in server, PLEASE CONTACT PE TO CHECK IF THE NGINX SERVER WAS CRASHED!!! Due to:"..v)  
    end
end

